/*
 * $Id$
 */
package corejs.rt;

import corejs.expr.ArrayExpr;
import corejs.expr.Expression;
import corejs.expr.NameExpr;
import corejs.expr.ObjectExpr;
import corejs.expr.RefinementExpr;
import corejs.expr.SequenceExpr;

import corejs.stmt.Block;
import corejs.stmt.FunctionDeclaration;
import corejs.stmt.Statement;
import corejs.stmt.StatementVisitor;
import corejs.stmt.VarDeclaration;
import corejs.stmt.Loop;

import java.math.BigDecimal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static corejs.rt.Bool.*;

/**
 *
 * @author myang
 */
public class Interpreter extends VisitorSupport {

   private static Interpreter _this = new Interpreter();

   public static Interpreter get() { return _this; }
 
   private RuntimeStack _rt = new RuntimeStack();

   public RuntimeStack getRuntimeStack() { return _rt; }

   private Value visit(final Expression expr) {
      return expr.accept(this);
   }
   
   private Interpreter() {}

   /**
    * 
    */
   private ExecutionContext _ec = new ExecutionContext() {
      @Override
      public Value get() {
         return get("this");
      }

      @Override
      public <T> T get(final Class<T> clz) {
         return get("this", clz);
      }
     
      @Override
      public Value get(final String name) {
         return _rt.resolve(name);
      }

      @Override
      public <T> T get(final String name, final Class<T> clz) {
         return get(name).cast(clz);
      }

      @Override
      public ExecutionContext put(final String name, final Value val) {
         _rt.currentScope().put(name, val);
         return this;
      }

      @Override
      public Number getNumber(final String name) {
         return Value.toPrimitive(get(name), this).toNumber();
      }

      @Override
      public Number getNumber(final String name, final int defaultInt) {
         try {
            final Number n = getNumber(name);
            return n.isNaN() ? Int.valueOf(defaultInt) : n;
         }
         catch (TypeError e) {
            return Int.valueOf(defaultInt);
         }
      }

      @Override
      public Str getStr(final String name) {
         return toStr(get(name));
      }

      @Override
      public Str toStr(final Value src) {
         return Value.toPrimitive(src, this).toStr();
      }
      
      @Override
      public StatementVisitor getStatementVisitor() {
         return Interpreter.this;
      }

      @Override
      public Value invoke(final Value obj, final String methodName,
                          final Value... args) {
         return this.invoke(obj, obj.resolve(methodName, Invocable.class), args);
      }
   
      @Override
      public Value invoke(final Value obj, final Invocable fn,
                          final Value... args) {
         return Interpreter.this.invoke(obj, fn, args);
      }

      @Override
      public ActivationRecord currentScope() {
         return _rt.currentScope();
      }
   };

   /**
    * 
    * @param expr
    * @return 
    */
   private Primitive toPrimitive(final Expression expr) {
      return Value.toPrimitive(visit(expr), _ec);
   }
 
   // spec 11.6.1
   @Override
   public Value visitAddition(final Expression left, final Expression right) {
      final Primitive op1 = toPrimitive(left);
      final Primitive op2 = toPrimitive(right);
      return op1.isStr() || op2.isStr() ? op1.toStr().concat(op2.toStr()) 
              :                           op1.toNumber().add(op2.toNumber());
   }

   @Override
   public Value visitMultiplication(final Expression left, final Expression right) {
      final Primitive op1 = toPrimitive(left), op2 = toPrimitive(right);
      return op1.isStr()    ? op2.toNumber().multiply(op1.toStr())
              : op2.isStr() ? op1.toNumber().multiply(op2.toStr())
              :               op1.toNumber().multiply(op2.toNumber());
   }

   // spec 11.6.2
   @Override
   public Value visitSubtraction(final Expression left, final Expression right) {
      final Primitive n1 = toPrimitive(left), n2 = toPrimitive(right);
      return n1.toNumber().subtract(n2.toNumber());
   }

   @Override
   public Bool visitAnd(final Expression left, final Expression right) {
      final Bool b1 = visit(left).toBool();
      final Bool b2 = visit(right).toBool();
      return b1.isTrue() && b2.isTrue() ? TRUE : FALSE;
   }


   @Override
   public Bool visitOr(final Expression left, final Expression right) {
      final Value op1 = visit(left), op2 = visit(right);
      return op1.toBool().isTrue() ? TRUE
         :   op2.toBool().isTrue() ? TRUE
         :                           FALSE;
   }

   @Override
   public Bool visitNot(final Expression e) {
      return visit(e).toBool().isTrue() ? FALSE : TRUE;
   }

   /**
    *
    * @param arrayExpr
    * @return
    */
   @Override
   public Array visitArray(final ArrayExpr arrayExpr) {
      final List<Value> vals = new ArrayList<Value>();
      for (final Expression expr : arrayExpr) {
         vals.add(visit(expr));
      }
      return new Array(vals);
   }

   /**
    *
    * @param target
    * @param right
    * @return
    */
   @Override
   public Value visitAssignment(final RefinementExpr target, 
                                final Expression right) {
      final Obj left = visit(target.left()).cast(Obj.class);
      return left.store(target.acceptRefinement(this).toString(), 
                        visit(right));
   }

   @Override
   public Value visitAssignment(final String name, final Expression e) {
      return _rt.store(name, visit(e));
   }

   @Override
   public Value visitRefinement(final Expression target, 
                                final Expression right) {
      final Primitive _right = toPrimitive(right);
      if (_right.isNumber()) {
         final ArrayAccessSupport _target = visit(target).cast(ArrayAccessSupport.class);
         return _target.resolve(_right.toNumber().intValue());
      }
      else if (_right.isStr()) {
        return visit(target).resolve(_right.toStr().toString());
      }
      throw new TypeError();
   }

   @Override
   public Function visitFunction(final String name, final String[] args, 
                                 final Block body) {
      return (Function)_rt.store(name, 
                                 new Function(_rt.currentScope(), name, args, body)); 
   }

   @Override
   public Int visitInteger(final int i) { 
      return Int.valueOf(i); 
   }

   @Override
   public Decimal visitDecimal(final BigDecimal d) { 
      return new Decimal(d); 
   }

   @Override
   public Long visitLong(final long l) { 
      return Long.valueOf(l); 
   }

   /**
    * 
    * @param e
    * @param args
    * @return 
    */
   @Override
   public Value visitInvocation(final Expression e, 
                                final List<Expression> args) {      
      final List<Value> arguments = new ArrayList<Value>() {{
         for (final Expression ex : args) {
            add(ex.accept(Interpreter.this));
         }
      }};
      
      Value ths = null;
      if (e instanceof RefinementExpr) {
         ths = visit(((RefinementExpr)e).left());
      }
            
      return invoke(ths, visit(e).cast(Invocable.class), 
                    arguments.toArray(new Value[0]));
   }


   /**
    * 
    * @param e
    * @return 
    */
   @Override
   public Value visitName(final NameExpr e) { return _rt.resolve(e.name()); }

   /**
    *
    * @param expr
    * @return
    */
   @Override
   public Value visitObject(final ObjectExpr expr) {
      return new Obj(Obj.prototype)
         .put(new HashMap<String, Value>() {{
         for (final Map.Entry<String, Expression> e : expr.entrySet()) {
            put(e.getKey(), visit(e.getValue()));
         }
      }});
   }

   /**
    *
    * @param seq
    * @return
    */
   @Override
   public Value visitSequence(final SequenceExpr seq) {
      for (int i = 0; i < seq.size() - 1; ++i) {
         visit(seq.get(i));
      }
      return visit(seq.get(seq.size() - 1));
   }
   
   /**
    *
    * @param s
    * @return
    */
   @Override
   public Str visitString(String s) {
      return new Str(s);
   }

   /**
    *
    * @param e
    * @return
    */
   @Override
   public Str visitTypeof(final Expression e) {
      return visit(e).typeof();
   }

   @Override
   public Undefined visitUndefined() {
      return Undefined.UNDEFINED;
   }

   @Override
   public void visitBlock(final List<FunctionDeclaration> fds, 
                          final List<VarDeclaration> vds, 
                          final List<Statement> stmts) {
      for (final FunctionDeclaration f : fds) {
        f.accept(this);
      }
      
      for (final VarDeclaration v : vds) {
         v.accept(this);
      }

      for (final Statement stmt : stmts) {
         stmt.accept(this);
      }
   }


   @Override
   public void visitVars(final List<String> names) {
      for (final String name : names) {
         _rt.storeLocally(name, Undefined.UNDEFINED);
      }
   }

   /**
    * 
    * @param expr
    * @return 
    */
   @Override
   public Value visitExpression(final Expression expr) {
      return visit(expr);
   }

   /**
    * 
    * @param expr 
    */
   @Override
   public void visitReturn(final Expression expr) {
      _rt.currentScope().setReturn(null == expr ? Undefined.UNDEFINED 
                                                : visit(expr));
      throw FlowReturnException.RETURN;
   }

   @Override
   public Bool visitBool(final boolean b) {
      return Bool.valueOf(b);
   }

   /**
    * spec 11.8.1, spec 11.8.5
    * http://www.ecma-international.org/ecma-262/5.1/#sec-11.8.1
    * @return 
    */
   @Override
   public Bool visitLessThan(final Expression left, final Expression right) {
      return isLessThan(left, right).toBool();
   }
   
   // spec 11.8.2
   @Override
   public Bool visitGreaterThan(final Expression left, final Expression right) {
      return isLessThan(right, left).toBool();
   }

   // spec 11.8.3
   @Override
   public Bool visitLessEqualThan(final Expression left, final Expression right) {
      final Value v = isLessThan(right, left);
      return v.isUndefined() || v.toBool().isTrue() ? FALSE : TRUE;
   }

   // spec 11.8.4
   @Override
   public Bool visitGreaterEqualThan(final Expression left, final Expression right) {
      final Value v = isLessThan(left, right);
      return v.isUndefined() || v.toBool().isTrue() ? FALSE : TRUE;
   }

   private Value isLessThan(final Expression left, final Expression right) {
      final Primitive x = toPrimitive(left), y = toPrimitive(right);
      if (x.isStr() && y.isStr()) {
         return x.toStr().compareTo(y.toStr()) < 0 ? TRUE : FALSE;
      }
      else {
         final Number n1 = x.toNumber(), n2 = y.toNumber();
         return n1.isNaN() || n2.isNaN()       ? Undefined.UNDEFINED
                 : n1.compareTo(n2).equals(-1) ? TRUE
                 :                               FALSE;
      }
   }
   
   // spec 11.9.1, 11.9.3
   // http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.1
   @Override
   public Bool visitEquals(final Expression left, final Expression right) {
      return Bool.valueOf(visit(left).equals(visit(right), _ec));
   }

   // spec 11.9.2, 11.9.3
   // http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.2
   @Override
   public Bool visitNotEquals(final Expression left, final Expression right) {
      return visitEquals(left, right).isTrue() ? FALSE : TRUE;
   }

   // spec 11.9.4, 11.9.6
   // http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.6
   @Override
   public Bool visitStrictEquals(final Expression left, final Expression right) {
      return Bool.valueOf(visit(left).equalsStrictly(visit(right)));
   }

   // spec 11.9.5, 11.9.6
   @Override
   public Bool visitStrictNotEquals(final Expression left, final Expression right) {
      return visitStrictEquals(left, right).isTrue() ? FALSE : TRUE;
   }
   
   @Override
   public Null visitNull() {return Null.NULL;}

   // spec 11.4.6, 11.4.7
   @Override
   public Number visitSignedExpr(final boolean neg, final Expression expr) {
      final Number n = toPrimitive(expr).toNumber();
      return neg ? n.neg() : n;
   }

   // spec 11.3.1, 11.4.4
   @Override
   public Value visitIncrement(final boolean prefix, final Expression expr) {
      return increment(prefix, expr, 1);
   }

   // spec 11.3.2, 11.4.5
   @Override
   public Value visitDecrement(final boolean prefix, final Expression expr) {
      return increment(prefix, expr, -1);
   }

   /**
    * 
    * @param prefix
    * @param expr
    * @param i
    * @return 
    */
   private Value increment(final boolean prefix, final Expression expr, final int i) {
      if (expr instanceof NameExpr) {
         final Number n = toPrimitive(expr).toNumber();
         final Value rt = _rt.store(((NameExpr)expr).name(), n.add(i));
         return prefix ? rt : n;
      }
      else if (expr instanceof RefinementExpr) {
         final RefinementExpr re = (RefinementExpr)expr;
         return increment(prefix, visit(re.left()), toPrimitive(re.right()), i);
      }      
      throw new TypeError();
   }
   
   /**
    * 
    * @param prefix
    * @param base
    * @param refinement
    * @param i
    * @return 
    */
   private Value increment(final boolean prefix, final Value base,
                           final Primitive refinement, final int i) {
      Number n = null, n1 = null;
      if (refinement.isNumber() && base instanceof ArrayAccessSupport) {
         final int index = refinement.toNumber().intValue();
         n = Value.toPrimitive(((ArrayAccessSupport)base).resolve(index), _ec)
               .toNumber();
         n1 = n.add(i);
         if (base instanceof Array) {
            ((Array)base).set(index, n1);
         }
      }
      else {
         final String name = refinement.toStr().toString();
         n = Value.toPrimitive(base.resolve(name), _ec).toNumber();
         n1 = (Number)base.store(name, n.add(i));
      }
      return prefix ? n1 : n;
   }

   // spec 11.12
   @Override
   public Value visitConditionExpr(final Expression test, final Expression then, 
                                   final Expression els) {
      return visit(test).toBool().isTrue() ? visit(then) : visit(els);
   }

   // spec 12.5
   @Override
   public void visitIf(final Expression test, final Statement then, 
                       final Statement els) {
      if (visit(test).toBool().isTrue()) {
         then.accept(this);
      }
      else if (null != els) {
         els.accept(this);
      }
   }

   @Override
   public void visitBreak(final String label) {
      throw FlowBreakException.BREAK;
   }   

   @Override
   public void visitContinue(final String label) {
      throw FlowContinueException.CONTINUE;
   }

   @Override
   public void visitLoop(final Loop loop) { loop.loop(this, this); }
  
   /**
    *
    * @param ths
    * @param fn
    * @param args
    * @return
    */
   public Value invoke(final Value ths, final Invocable fn, final Value... args) {
      final String[] argNames = fn.getArgumentNames();
      return invoke(fn, new HashMap<String, Value>() {{
         for (int i = 0; i < argNames.length; ++i) {
            final String argName = argNames[i];
            if ("*".equals(argName)) {
               for (; i < args.length; ++i) {
                  put(String.valueOf(i), args[i]);
               }
               break;
            }
            else {
               put(argName, i < args.length ? args[i] : Undefined.UNDEFINED);
            }
         } 
         put("this", null == ths ? _rt.getGlobalObject() : ths); 
         put("arguments", new Arguments(args, fn));
      }});
   }

   /**
    *
    * @param fn
    * @param args
    * @return
    */
   public Value invoke(final Invocable fn, final Map<String, Value> args) {
      final ActivationRecord ar = fn.parentScope();
      _rt.newScope(null == ar ? _rt.currentScope() : ar, args);
      final Value val = fn.invoke(_ec);
      _rt.popScope();
      return val == null ? Undefined.UNDEFINED : val;
   }

}
